﻿// Copyright (C) 2016 by David Jeske, Barend Erasmus and donated to the public domain

using OpenHttpServer.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Reflection;
using System.Xml.Linq;
using System.Web;

namespace OpenHttpServer
{
    public class HttpProcessor
    {

        #region Fields

        private List<Route> Routes = new List<Route>();

        //private static readonly I
        //log = LogManager.GetLogger(typeof(HttpProcessor));

        #endregion

        #region Constructors

        public HttpProcessor()
        {
        }

        #endregion

        #region 公共方法
        public void HandleClient(TcpClient tcpClient)
        {
                Stream? inputStream = GetInputStream(tcpClient);
                Stream? outputStream = GetOutputStream(tcpClient);
                HttpRequest request = GetRequest(inputStream, outputStream, tcpClient.Client.RemoteEndPoint.ToString());

                // route and handle the request...
                HttpResponse response = RouteRequest(inputStream, outputStream, request);      
          
                //Console.WriteLine("{0} {1}",response.StatusCode,request.Url);
                // build a default response for errors
                if (response.Content == null) {
                    if (response.StatusCode != 200) {
                        response.ContentAsUTF8 = string.Format("{0} {1} <p> {2}", response.StatusCode, request.Url, response.ReasonPhrase);
                    }
                }

                WriteResponse(outputStream, response);

                outputStream.Flush();
                outputStream.Close();
                outputStream = null;

                inputStream.Close();
                inputStream = null;

        }

        // this formats the HTTP response...
        private static void WriteResponse(Stream stream, HttpResponse response) {            
            if (response.Content == null) {           
                response.Content = new byte[]{};
            }
            
            // default to text/html content type
            if (!response.Headers.ContainsKey("Content-Type")) {
                response.Headers["Content-Type"] = "application/VIID+JSON";
            }

            response.Headers["Content-Length"] = response.Content.Length.ToString();

            Write(stream, string.Format("{0} {1} {2}\r\n",response.ProtocolVersion, response.StatusCode,response.ReasonPhrase));
            Write(stream, string.Join("\r\n", response.Headers.Select(x => string.Format("{0}: {1}", x.Key, x.Value))));
            Write(stream, "\r\n\r\n");


            WriteStream(stream, response.Content);
            //stream.Write(response.Content, 0, response.Content.Length);       
        }

        public void AddRoute(Route route)
        {
            this.Routes.Add(route);
        }

        #endregion

        #region 私有方法

        private static string Readline(Stream stream)
        {
            int next_char;
            string data = "";
            while (true)
            {
                next_char = stream.ReadByte();
                if (next_char == '\n') { break; }
                if (next_char == '\r') { continue; }
                if (next_char == -1) { Thread.Sleep(1); continue; };
                data += Convert.ToChar(next_char);
            }
            return data;
        }

        private static void Write(Stream stream, string text)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(text);
            WriteStream(stream, bytes);
            //stream.Write(bytes, 0, bytes.Length);
        }

        private static void WriteStream(Stream stream, byte[] bytes)
        {
            try
            {
                stream.Write(bytes, 0, bytes.Length);
            }catch(Exception ex)
            {
#if DEBUG
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("流写入错误 :{0}",ex.Message);
#endif
            }
        }

        protected virtual Stream GetOutputStream(TcpClient tcpClient)
        {
            return tcpClient.GetStream();
        }

        protected virtual Stream GetInputStream(TcpClient tcpClient)
        {
            return tcpClient.GetStream();
        }

        protected virtual HttpResponse RouteRequest(Stream inputStream, Stream outputStream, HttpRequest request)
        {
            if (request == null)
                return HttpBuilder.NotSupported();

            List<Route> routes = this.Routes.Where(x => Regex.Match(request.Url, x.UrlRegex).Success).ToList();

            if (!routes.Any())
                return HttpBuilder.NotFound();

            Route route = routes.SingleOrDefault(x => x.Method == request.Method);

            if (route == null)
                return new HttpResponse()
                {
                    ReasonPhrase = "Method Not Allowed",
                    StatusCode = 405,

                };

            // extract the path if there is one
            var match = Regex.Match(request.Url,route.UrlRegex);
            if (match.Groups.Count > 1) {
                request.Path = match.Groups[1].Value;
            } else {
                request.Path = request.Url;
            }

            // trigger the route handler...
            request.Route = route;
            try {
                return route.Callable(request);
            } catch(Exception ex) {
#if DEBUG
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("路由解析错误:{0},{1}" ,ex.Message, request.Url);
#endif
                return HttpBuilder.InternalServerError();
            }

        }

        private HttpRequest GetRequest(Stream inputStream, Stream outputStream,string remoteaddress)
        {
            //Read Request Line
            string request = Readline(inputStream);

            string[] tokens = request.Split(' ');
            if (tokens.Length != 3)
            {
                return null;
                //throw new Exception("无效的http请求行");
            }
            HttpMethods enums;
            Enum.TryParse(tokens[0].ToUpper(), true, out enums);
            var method = (enums==null)? HttpMethods.POST: enums; //tokens[0].ToUpper();
            string url = tokens[1];
            string protocolVersion = tokens[2];


            //Read Headers
            Dictionary<string, string> headers = new Dictionary<string, string>();
            Dictionary<string, string> posts = new Dictionary<string, string>();
            Dictionary<string, string> gets = new Dictionary<string, string>();

            string line;
            while ((line = Readline(inputStream)) != null)
            {
                if (line.Equals(""))
                {
                    break;
                }

                int separator = line.IndexOf(':');
                if (separator == -1)
                {
                    throw new Exception("invalid http header line: " + line);
                }
                string name = line.Substring(0, separator);
                int pos = separator + 1;
                while ((pos < line.Length) && (line[pos] == ' '))
                {
                    pos++;
                }

                string value = line.Substring(pos, line.Length - pos);
                if(!headers.ContainsKey(name))
                    headers.Add(name, value);
            }

            string content = String.Empty;
            byte[] bytes=new byte[] { };
            if (headers.ContainsKey("Content-Length"))
            {
                int totalBytes = Convert.ToInt32(headers["Content-Length"]);
                int bytesLeft = totalBytes;
                bytes = new byte[totalBytes];
               
                while(bytesLeft > 0)
                {
                    byte[] buffer = new byte[bytesLeft > 1024? 1024 : bytesLeft];
                    int n = inputStream.Read(buffer, 0, buffer.Length);
                    buffer.CopyTo(bytes, totalBytes - bytesLeft);

                    bytesLeft -= n;
                }

                content = Encoding.UTF8.GetString(bytes);
            }

            var reg = new Regex(@"(^|&)?(\w+)=([^&]+)(&|$)?", RegexOptions.Compiled);
            MatchCollection _get = reg.Matches(url);

            if (_get.Count > 0)
            {
                foreach (Match m in _get)
                {
                    var _k = HttpUtility.UrlDecode(m.Groups[2].ToString(), Encoding.UTF8);
                    var _v = HttpUtility.UrlDecode(m.Groups[3].ToString(), Encoding.UTF8);
                    if(!gets.ContainsKey(_k))
                        gets.Add(_k, _v);
                }
            }

            if (method == HttpMethods.POST || method == HttpMethods.DELETE || method == HttpMethods.PUT)
            {
                //reg = new Regex(@"(\w+)=([^&]+)(&|$)?", RegexOptions.Compiled);
                MatchCollection _post = reg.Matches(content);
                if (_post.Count > 0)
                {
                    foreach (Match m in _post)
                    {
                        var _k = HttpUtility.UrlDecode(m.Groups[2].ToString(), Encoding.UTF8);
                        var _v = HttpUtility.UrlDecode(m.Groups[3].ToString(), Encoding.UTF8);
                        if (!gets.ContainsKey(_k))
                            posts.Add(_k, _v);
                    }
                }

            }

            var result = new HttpRequest()
            {
                Method = method,
                Url = url,
                Headers = headers,
                Content = content,
                Data = bytes,
                RemoteAddress = remoteaddress,
                Posts = posts,
                Gets = gets,
                ProtocolVersion = protocolVersion,
                //Route = Route
            };
            return result;
        }

        #endregion


    }
}
